iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Software Development

30天打造一套企業PLM系列 第 4

Day 4:Metadata-driven—不改程式就能加欄位

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260821/20161290WEsU8QS4fl.jpg

系列:30 天打造企業級 PLM|面向:全端|素材:Config 模組、視覺化表單設計器

問題場景

商用 PLM 最強的不是功能多,是管理員自己就能加欄位。品保明天要在表單上多一欄「檢驗標準」,不用等 IT 排程、不用改版部署,設定完存檔就生效。自建 PLM 若做不到這件事,那只是做了一套要一直改程式的表單系統。今天講 Mini-PLM 的 metadata-driven 架構:欄位定義存在資料庫裡,前後端都照定義行事。

商業邏輯設計

  • 欄位需求永遠長不完。研發要規格參數、採購要供應商資訊、品保要檢驗標準,而且每個表單類型、每個品項類型要的都不一樣
  • 管理員自助的營運模式,讓 IT 從「改欄位的人」變成「治理規則的人」。欄位誰能加、加了誰看得到,才是 IT 要管的事
  • 欄位下架的治理:停用欄位的舊資料必須還讀得到,只能 disable 不能刪

技術選型與取捨:動態欄位的三條路,與「定義」vs「儲存」的分工

動態欄位儲存的三條路:為什麼 Mini-PLM 與 Agile 都選寬表?

動態欄位的儲存有三種經典路線,說穿了是資料庫正規化與反正規化的取捨:

路線 作法 優點 代價
EAV(Entity-Attribute-Value) 一張值表存所有欄位值,一列一值 完全正規化、加欄位零成本、無數量上限 查詢要大量 JOIN/pivot、型別全變字串、千筆表單查詢效能雪崩
JSON 欄位 實體上一個 JSON column 裝所有動態值 讀寫單純、結構自由 跨庫支援度不一(Oracle/PG 語法與索引差異大)、動態 Criteria 查詢受限
寬表(Wide Table) 預先挖好型別欄位池(text01..50, date01..30 等) 原生 SQL 查詢最快、一對一關聯直讀、跨庫完全相容、索引好下 實體欄位有物理上限、需要定義層做槽位映射

https://ithelp.ithome.com.tw/upload/images/20260821/20161290QTLw8TtsEB.jpg

在資料儲存層,Mini-PLM 與 Agile 做出了相同的務實選擇:採用「寬表欄位池」mp_form_datamp_item_data)。

mp_form_data 為例,資料表預先挖好了各型態的實體欄位池:

  • text01..text50
  • textarea01..textarea30
  • date01..date30
  • select01..select30 / multilist01..multilist30
  • radio01..radio30 / checkbox01..checkbox30
  • number01..number30
  • image01..image05 / upload01..upload05
  • html01..html10
  • OTHER_INPUT_VALUES

為什麼不選純 EAV? 如果一張表單有 30 個自訂欄位,純 EAV 查 1,000 筆表單需要掃描 30,000 列並做 30 次 self-JOIN 或記憶體 pivot,資料庫效能直接崩潰。而寬表只需要一次一對一的主鍵 JOIN,效能與原生實體表完全相同。


定義機制的跨時代對決:Agile vs. Mini-PLM

雖然在「資料儲存層」兩者都選擇了寬表,但在**「欄位定義機制(Metadata Definition)」**與前後端協同上,Mini-PLM 與舊時代 Agile 有著本質上的演進:

比較維度 舊時代 Oracle Agile (Java Client / Flex Attributes) 現代 Mini-PLM (Metadata-Driven Engine)
定義架構 深度綁死在專有 Java Client Admin 階層與私有 Data Dictionary(Admin Nodes / Page Two / Page Three) 乾淨正規化的 JPA 定義表mp_config_form_field / mp_config_item_field),定義本身就是標準資料
前後端連動 JSP 伺服器端標籤庫與 Java Client (Swing) 雙軌各自解析,前後端校驗邏輯易脫鉤 單一事實來源(Single Source of Truth):一份定義 JSON 同時驅動後端驗證(Start Gate)與前端 ProForm 宣告式渲染
管理維運體驗 必須開啟笨重的 Java Client 視窗,一層層點開屬性樹逐一填寫,無法即時預覽版面 雙操作模式:既有快速維護表格 + Canva 式所見即所得(WYSIWYG)拖拉表單設計器,排版所見即所得
流程關卡連動 欄位在不同關卡的顯隱與唯讀需仰賴複雜的 Privilege / Criteria 甚至寫 Java PX 原生內建 step 屬性:在定義中直接宣告該欄位在哪個簽核關卡顯示/必填/編輯,零程式碼達成關卡級動態欄位
排版與分組 版面固定由系統排版(雙欄或固定表格),無法自由分區 原生內建 groups(分區卡片)與 col_per_row(響應式欄數),靈活配置前端版面
動態子表格 Table 欄位配置死板,擴充二維結構極其複雜 雙層定義架構:獨立表頭(mp_config_table_header)與欄位(mp_config_table_column),在寬表上優雅支撐二維動態子表

核心內容

欄位定義與槽位映射架構

https://ithelp.ithome.com.tw/upload/images/20260821/20161290jDuMuzSVAX.jpg

mp_config_form_field(節錄自資料庫綱要)就是一個欄位的完整說明書:

mp_config_form_field
 field_type        varchar(45)   ← text/textarea/digit/date/select/multilist/radio/checkbox/image/upload/html/table
 data_index        varchar(45)   ← 欄位 key(後端存值、前端取值都認它)
 field_name        varchar(200)  ← 顯示名稱
 required          boolean       ← 必填(後端 Start Gate 與前端 rules 共用)
 visible           boolean
 pattern           varchar(100)  ← 正規表達式驗證
 pattern_msg       varchar(100)  ← 驗證失敗訊息
 default_value     varchar(500)
 col_per_row       integer       ← 版面:一列幾欄
 groups            varchar(45)   ← 欄位分組(畫面上的區塊)
 step              integer       ← 綁定流程關卡(哪一關才出現/可編輯)
 order_by          integer       ← 排序
 form_type_id      FK → mp_config_form_type
 listnode_id       FK → mp_config_listnode      ← select/multilist 的選項來源
 config_table_header_id FK → mp_config_table_header ← table 型欄位的表頭定義

留意三個「一份定義、兩端共用」的欄位。requiredpattern 同時驅動前端 antd rules 與後端驗證;listnode_id 同時餵前端下拉選項與後端存值合法性檢查;step 讓同一張表單在不同簽核關卡長出不同的欄位。定義只存一份,前後端各自解讀,永遠不會不同步。這就是 metadata-driven 的核心:單一事實來源。

流程關卡動態連動與 Start Gate 閘門防護

https://ithelp.ithome.com.tw/upload/images/20260821/20161290cNYuq6oiKI.jpg

除了畫面排版,Metadata-driven 的威力在於流程關卡連動

  • 宣告式關卡控制 (step):同一個欄位在草稿階段(Step 1)必填,到了主管審核階段(Step 2)自動轉為唯讀,而品保專屬的「檢驗標準」只有在流程推進到品保關卡(Step 3)時才會動態浮現。
  • 後端 Start Gate 嚴格驗證:前端畫面動態隱藏/唯讀只是防君子,安全防線在後端。當表單送出審核時,後端 Start Gate 依據當前關卡嚴格比對 Metadata 定義,阻擋跨關卡未授權竄改並校驗必填項,確認資料無誤才落入實體寬表槽位。

從表格模式到 Canva 式視覺設計器

欄位定義的維護介面有兩種模式,同一份資料、兩種操作體驗(ConfigForm.tsx):

// 下方區塊顯示模式:table(既有 ConfigFields 表格)或 visual(WYSIWYG 視覺設計器)
const [mode, setMode] = useState<"table" | "visual">("table");

視覺設計器實機畫面。左側元件盤(每種欄位型態一個積木,數字是既有數量)、中間畫布(依 groups 分區、拖拉排序)、右側屬性面板:

https://ithelp.ithome.com.tw/upload/images/20260821/20161290YDTvPOnnOn.png

拖拉核心用 @dnd-kit,架構上只有一個關鍵約束,VisualDesigner.tsx 開頭的註解就是設計文件:

// 三欄組裝:左工具箱、中畫布、右屬性面板。
// 唯一的 DndContext 置於此層,同時包住 Toolbox 與 Canvas,
// 兩者的拖放事件才會互相辨識。
<DndContext
  sensors={sensors}
  collisionDetection={pointerFirstCollision}
  // 插入槽在拖曳開始才撐高:需持續重新量測 droppable rect,
  // 否則命中框停留在撐高前的位置
  measuring={{ droppable: { strategy: MeasuringStrategy.Always } }}
  onDragStart={handleDragStart}
  onDragEnd={handleDragEnd}
>

順帶一提,這個設計器就是 Day 1 說的 AI Coding 實績。從傳統表格模式進化到所見即所得拖拉介面,過去要排一季的功能,實際只花不到一週。能這麼快落地的前提,正是 metadata 層早就定義完整:設計器只是同一份欄位定義的另一種編輯器,後端一行都不用動。

踩坑記錄

  • multilist / checkbox 存的是 JSON 陣列字串(如 ["A","B"])。出口做 label 轉換時必須 JSON-aware 解析再以 ", " join,直接當字串輸出,前端會顯示成無分隔的連接字
  • 欄位改名(data_index)等於換 key,舊資料的值還掛在舊 key 上。欄位 key 一旦有資料就別再改,要改改顯示名稱(field_name)就好
  • 欄位分組(groups)改了之後前端快取沒失效,畫面上欄位「消失」。這條線牽到 Day 19 的 meta 快取失效機制

小結

定義層正規化換彈性與單一事實來源,值層務實採用寬表槽位池換查詢極致效能。這套地基後面會一直回收:Day 8 動態表單渲染、Day 16 動態查詢、Day 30 的 AI for Setup,全部建立在「規則是資料」之上。

明日 Day 5:REST API 設計與統一回應格式——「單筆也包陣列」的教訓。


上一篇
Day 3:PLM 領域模型怎麼設計
系列文
30天打造一套企業PLM4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言